```tex
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Copyright by Wenliang Du.                                        %%
%% This work is licensed under the Creative Commons                %%
%% Attribution-NonCommercial-ShareAlike 4.0 International License. %%
%% To view a copy of this license, visit                           %%
%% http://creativecommons.org/licenses/by-nc-sa/4.0/.              %%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

\input{../../common-files/header}
\input{../../common-files/copyright}

\newcommand{\telnet} {\texttt{telnet}\xspace}

\newcommand{\firewallFigs}{./Figs}
\lhead{\bfseries SEED Labs -- Linux Firewall Exploration Lab}

\begin{document}



\begin{center}
{\LARGE Linux Firewall Exploration Lab}
\end{center}

\seedlabcopyright{2006 - 2016}


% *******************************************
% SECTION
% ******************************************* 
\section{概述}

本实验室的学习目标是让学生通过操作防火墙软件并实现一个简化的包过滤防火墙，从而获得对防火墙工作原理的深入了解。防火墙有多种类型，在此实验中我们将重点介绍 \textit{包过滤} 类型的防火墙。包过滤防火墙会检查数据包，并根据防火墙规则决定是否丢弃或转发该数据包。这些防火墙通常是非状态化的；它们仅基于每个数据包中的信息来过滤数据包，而不考虑该数据包是否属于现有的流量流。

包过滤防火墙常常结合使用源地址和目的地址、协议类型，以及对 TCP 和 UDP 流量的端口号进行判断。在本实验中，学生将操作这种类型的防火墙，并通过实现一些关键功能来了解防火墙的工作原理。此外，学生还将学习如何使用 SSH 隧道绕过防火墙。本实验涵盖了以下主题：

\begin{itemize}[noitemsep]
    \item 防火墙
    \item Netfilter
    \item 可加载内核模块
    \item 使用 SSH 隧道绕过防火墙
    \item 使用 VPN 绕过防火墙的内容将在单独的实验中覆盖。
\end{itemize}

\paragraph{参考资料和视频。}关于防火墙的详细内容可以在以下资料中找到：

\begin{itemize}
    \item 《SEED Book》第17章，\seedbook
    \item 《SEED Lecture》第9节，\seedisvideo
\end{itemize}

\paragraph{实验环境。} \seedenvironmentB


% *******************************************
% SECTION
% ******************************************* 
\section{实验任务}



% -------------------------------------------
% SUBSECTION
% ------------------------------------------- 
\subsection{任务 1：使用防火墙}

Linux 内置了一个名为 {\tt iptables} 的工具，本质上就是一种防火墙。在本任务中，目标是利用 {\tt iptables} 设置一些防火墙策略，并观察设置这些策略后的系统行为。你需要至少运行两个虚拟机（VM），一个称为 Machine A，另一个称为 Machine B。你将在 Machine A 上部署防火墙。基本上，我们将使用 {\tt iptables} 作为个人防火墙。如果你有更多的 VM，还可以在路由器上设置防火墙以保护整个网络，而不仅仅是单个计算机。在你设置了这两个虚拟机后，你应该执行以下任务：

\begin{itemize}
    \item 阻止 Machine A 使用 \telnet 访问 Machine B。
    \item 阻止 Machine B 使用 \telnet 访问 Machine A。
    \item 阻止 Machine A 访问外部网站。你可以选择任何你想要阻止的网站，但请记住，一些 Web 服务器可能有多个 IP 地址。
\end{itemize}

你可以通过在终端中输入 {\tt "man iptables"} 或在线搜索来查找 {\tt iptables} 的手册。我们列出了以下常用命令：

\begin{lstlisting}
// 列出 filter 表中的所有规则
$ sudo iptables -L
$ sudo iptables -L --line-numbers

// 删除 filter 表中的所有规则
// 注意：请避免这样做，因为 Docker 已设置了大量防火墙规则；删除它们可能会导致问题。
$ sudo iptables -F

// 从 INPUT 链中删除第 2 条规则
$ sudo iptables -D INPUT 2

// 跳过满足 <rule> 的所有传入数据包
$ sudo iptables -A INPUT <rule>  -j DROP
\end{lstlisting}

 

% -------------------------------------------
% SUBSECTION
% ------------------------------------------- 
\subsection{任务 2：实现一个简单的防火墙} 

你在上一任务中使用的防火墙是一种包过滤类型的防火墙。这种防火墙的主要部分是过滤器，它会检查每个传入和传出的数据包，并根据管理员设置的策略进行强制执行。由于数据包处理是在内核中完成的，因此过滤也需要在内核中完成。因此，实现这样一个防火墙似乎需要我们修改 \Linux 内核。在过去，这必须通过修改并重新构建内核来完成。现代 Linux 操作系统提供了一些新的机制，可以在不重建内核镜像的情况下方便地操作数据包。这些两种机制是 Loadable Kernel Module ({\tt LKM}) 和 {\tt Netfilter}。

{\tt LKM} 允许我们在运行时向内核添加一个新的模块。这个新模块使我们能够扩展内核的功能，而无需重新编译内核或重启计算机。防火墙的包过滤部分可以实现为一个 LKM。然而，这还不足够。为了阻止传入/传出的数据包，该模块必须被插入到数据包处理路径中。在 {\tt Netfilter} 引入之前，这是无法轻易完成的任务。

{\tt Netfilter} 设计用于授权用户方便地操作数据包。它通过在 Linux 内核中实现一系列 {\em hooks} 来达成这个目标。这些钩子被插入到各种地方，包括数据包的传入和传出路径。如果我们想操纵传入的数据包，我们只需将自己的程序（内置于 LKM 中）连接到相应的钩子即可。一旦传入数据包到达，我们的程序将被调用。程序可以决定是否阻止该数据包；此外，我们还可以在程序中修改这些数据包。

在这项任务中，你需要使用 LKM 和 {\tt Netfilter} 来实现一个包过滤模块。这个模块会从一个数据结构中获取防火墙策略，并根据这些策略决定是否应阻塞数据包。为了使你的生活更轻松，以便你可以专注于核心过滤部分——即防火墙的核心，我们允许你将防火墙策略硬编码在程序中。你应该至少支持五种不同的规则，包括上一任务中指定的规则。关于如何使用 {\tt Netfilter} 的说明可以在 Section~\ref{firewall:sec:guidelines} 中找到。此外，《SEED Book》第17章还提供了更多关于 {\tt Netfilter} 的详细解释。

\paragraph{注意（针对 Ubuntu 20.04 VM）：}
《SEED Book》中的代码是在 Ubuntu 16.04 上开发的，需要稍作修改才能在 Ubuntu 20.04 上正常工作。这种修改主要涉及注册和注销钩子的 API。参见以下差异：

\begin{lstlisting}
// 钩子注册：
  nf_register_hook(&nfho);                  // 对于 Ubuntu 16.04 VM
  nf_register_net_hook(&init_net, &nfho);   // 对于 Ubuntu 20.04 VM


// 钩子注销：
  nf_unregister_hook(&nfho);                // 对于 Ubuntu 16.04 VM
  nf_unregister_net_hook(&init_net, &nfho); // 对于 Ubuntu 20.04 VM
\end{lstlisting}

 

% -------------------------------------------
% SUBSECTION
% ------------------------------------------- 
\subsection{任务 3：规避出口过滤}

许多公司和学校实施出口过滤，这会阻止内部网络中的用户访问某些网站或互联网服务。但通常允许用户访问其他网站。在大多数情况下，这种类型的防火墙会在传出数据包的目的 IP 地址和端口号上进行检查。如果数据包匹配了限制条件，则会被丢弃。出于性能原因，它们通常不会进行深入的数据包检查（即查看数据部分的内容）。在这项任务中，我们将展示如何使用隧道机制规避这种出口过滤。有多种方法可以建立隧道；在本任务中，我们仅关注 SSH 隧道。

你需要两个 VM A 和 B 来完成这项任务（最好有三个）。Machine A 在防火墙后面运行（即在公司的或学校的网络内部），而 Machine B 则在防火墙之外。通常会有一台专用的机器运行防火墙，但在本任务中为了方便起见，你可以在 Machine A 上运行防火墙。

你可以使用 {\tt iptables} 设置以下两个防火墙规则：

\begin{itemize}
    \item \underline{阻止所有传出流量到达外部 telnet 服务器。} 实际上，被阻止的通常是游戏服务器或其他可能影响员工生产力的服务器。在这项任务中我们仅用于示例目的使用了 telnet 服务器。你可以将 telnet 服务器运行在 Machine B 上。
    如果你有第三个 VM，Machine C，你可以在 Machine C 上运行 telnet 服务器。

    \item \underline{阻止所有传出流量到达 {\tt www.facebook.com}。} 这样员工或学生在工作/学习时间内不会被社交媒体吸引分神。公司和学校通常会阻塞社交网站。设置好防火墙后，请启动你的 Firefox 浏览器，尝试连接到 Facebook，并报告发生了什么情况。如果你已经使用该浏览器访问过 Facebook，则需要清除缓存，使用 Firefox 的菜单：\texttt{Edit -> Preferences -> Privacy \& Security (left pane) -> Clear History (Button on right)}；否则，可能会显示缓存的页面。如果一切设置正确，你应该无法看到 Facebook 页面。需要注意的是，Facebook 有多个 IP 地址，并且会随着时间变化而改变。请使用 {\tt ping} 或 {\tt dig} 命令检查地址是否仍然相同。如果地址已更改，请更新你的防火墙规则。你也可以选择使用具有静态 IP 的网站而不是 Facebook。例如，大多数大学的 Web 服务器都使用静态 IP 地址（如 {\tt www.syr.edu}），为演示目的，你可以尝试阻塞这些 IP 而不是 Facebook。
\end{itemize}

%--------------------------------
\paragraph{任务 3.a：通过防火墙 Telnet 到 Machine B}

如果使用三个 VM，则这项任务需要你从 Machine A 通过防火墙 Telnet 到 Machine C。如果你只用了两个 VM，则此任务如标题所示，即 Telnet 到 Machine B 通过防火墙进行。为了绕过防火墙，我们可以在 Machine A 和 B 之间建立一个 SSH 隧道，这样所有 Telnet 流量都会经过这个隧道（加密），从而绕过了检查。图~\ref{firewall:fig:sshtunnel} 展示了隧道的工作原理。

以下命令在本地主机端口 8000 和 Machine B 使用默认端口 22 之间建立了一个 SSH 隧道；当数据包从 B 端出来时，它将被转发到 Machine C 的端口 23（Telnet 端口）。如果你只有两个 VM，则可以使用一个 VM 同时作为 Machine B 和 Machine C。

\begin{lstlisting}
$ ssh -L 8000:Machine_C_IP:23  seed@Machine_B_IP

// 我们现在可以通过隧道 Telnet 到 Machine C:
$ telnet localhost 8000
\end{lstlisting}

当我们 \telnet 至 \texttt{localhost} 的端口 \texttt{8000} 时，SSH 会将从隧道一端的本地主机：8000 发出的所有 TCP 数据包转发到隧道另一端的 Machine B；之后，数据包会被转发出至 \texttt{Machine\_C:23}。来自 Machine C 的回复会逆向路径返回，并最终到达 Telnet 客户端。
本质上，我们能够通过隧道 Telnet 到 Machine C。请描述你的观察并解释你是如何绕过出口过滤的。你应该使用 Wireshark 查看线路上究竟发生了什么。

%--------------------------------
\paragraph{任务 3.b：通过 SSH 隧道连接到 Facebook}

为了实现这个目标，我们可以采用类似于 Task 3.a 的方法，即在你本地主机：端口与 Machine B 之间建立一个隧道，并让 B 转发数据包至 Facebook。为此，你可以使用以下命令设置隧道：
{\tt "ssh -L 8000:FacebookIP:80 ..."}. 
我们不会采用这种方法，而是会采用一种更通用的方法，称为动态端口转发，而不是像 Task 3.a 中的静态方法。为了实现这一点，我们只需要指定本地端口号而不需要最终目的地。当 Machine B 收到来自隧道的数据包时，它将根据数据包的目的信息动态决定应将其转发出至何处。

\begin{lstlisting}[backgroundcolor=]
$ ssh -D 9000 -C seed@machine_B
\end{lstlisting}

\begin{figure}[htb]
  \centering
  \includegraphics[width=0.8\textwidth]{\firewallFigs/SOCKS_config.pdf}
  \caption{配置 SOCKS 代理}
  \label{firewall:fig:socks_config}
\end{figure}

类似于 Telnet 程序连接至 {\tt localhost:9000}，我们还需要告诉 Firefox 每当它需要连接到 Web 服务器时都应连接到 {\tt localhost:9000}，这样流量可以经过我们的 SSH 隧道。为此，我们可以告诉 Firefox 使用 {\tt localhost:9000} 作为其代理。为了支持动态端口转发，我们需要一种特殊的代理类型称为 \textit{SOCKS 代理}，这是大多数浏览器都支持的。要在 Firefox 中设置代理，请点击菜单栏中的 \texttt{Edit -> Preferences}，滚动到 \texttt{Network Proxy} 并点击 \texttt{Settings} 按钮。然后遵循图~\ref{firewall:fig:socks_config}。

完成设置后，请进行以下操作：

\begin{enumerate}
    \item 运行 Firefox 并访问 Facebook 页面。
    你能看到 Facebook 页面吗？请描述你的观察。

    \item 在你获得 Facebook 页面之后，中断 SSH 隧道，并清除 Firefox 缓存，再次尝试连接。请描述你的观察。

    \item 再次建立 SSH 隧道并连接到 Facebook。请描述你的观察。

    \item 请解释你所观察到的现象，特别是为什么 SSH 隧道可以帮助绕过出口过滤。你应该使用 Wireshark 查看线路上究竟发生了什么，并描述你在捕获的包中观察到的情况。
\end{enumerate}

% -------------------------------------------
% SUBSECTION
% ------------------------------------------- 
\subsection{任务 4：规避入口过滤}


Machine A 在防火墙后运行一个 Web 服务器；因此，只能内部网络中的机器才能访问此 Web 服务器。你在家工作并需要访问这个内部的 Web 服务器。虽然没有使用 VPN，但你可以使用 SSH 进行连接，这被视为一种廉价的替代方案（即，Poor Man's VPN）。你有一个在 Machine A 或防火墙后的另一台内网机器上的账户，但防火墙也阻止了入站 SSH 连接，因此你无法通过 SSH 连接到内部网络中的任何机器。否则，你可以使用 Task 3 中的技术访问该 Web 服务器。然而，防火墙并没有阻止出站的 SSH 连接，也就是说，如果你想连接到外部的 SSH 服务器，你仍然可以这样做。

本任务的目标是能够从外面访问 Machine A 上的 Web 服务器。我们将使用两台机器来模拟这种设置。Machine A 是内部计算机，运行受保护的 Web 服务器；而 Machine B 则是在家中的外部机器。在 Machine A 上，我们阻止了 Machine B 从其端口 80（Web 服务器）和 22（SSH 服务器）访问。你需要在 Machine A 上设置一个反向 SSH 隧道，以便一旦你回到家，仍可以从家中访问受保护的 Web 服务器。

% *******************************************
% SECTION
% ******************************************* 
\section{指南}
\label{firewall:sec:guidelines}

\subsection{可加载内核模块}

以下是一个简单的可加载内核模块。当该模块被加载时会打印出 \texttt{"Hello World!"}；当该模块从内核中移除时，它会打印出 \texttt{"Bye-bye World!"}。这些消息不会显示在屏幕上；它们实际上是打印到 \texttt{/var/log/syslog} 文件中的。你可以使用 \texttt{"dmesg | tail -10"} 读取最后十条消息。

\begin{lstlisting}
#include <linux/module.h>
#include <linux/kernel.h>

int init_module(void)
{
    printk(KERN_INFO "Hello World!\n");
    return 0;
}

void cleanup_module(void)
{
    printk(KERN_INFO "Bye-bye World!.\n");
}
\end{lstlisting}

我们现在需要创建一个 {\tt Makefile}，其中包括以下内容（上述程序名为 {\tt hello.c}）。然后只需键入 {\tt make} 就可以将该程序编译为可加载内核模块。

\begin{lstlisting}
obj-m += hello.o

all:
        make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
        make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
\end{lstlisting}

一旦通过键入 {\tt make} 构建模块，你就可以使用以下命令来加载模块、列出所有模块以及移除模块。

\begin{lstlisting}
$ sudo insmod mymod.ko        (插入一个模块)
$ lsmod                       (列出所有模块)
$ sudo rmmod mymod.ko         (移除该模块)
$ dmesg | tail -10            (检查消息)
\end{lstlisting}

你还可以使用 {\tt modinfo mymod.ko} 来查看 Linux 内核模块的信息。

% -------------------------------------------
% SUBSECTION
% ------------------------------------------- 
\subsection{一个简单的 Netfilter 使用程序}

使用 {\tt Netfilter} 非常简单。我们只需要将我们的函数（在内核模块中）钩接到相应的 {\tt Netfilter} 钩子即可。这里展示了一个示例：

\begin{lstlisting}
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/netfilter.h>
#include <linux/netfilter_ipv4.h>

/* 这是我们将用于注册我们的函数的结构 */
static struct nf_hook_ops nfho;

/* 这是我们自己的钩子函数 */
unsigned int hook_func(void *priv, struct sk_buff *skb,
                       const struct nf_hook_state *state)

{
    /* 你可以在由 skb 指向的数据包中检查内容，并决定是否接受或丢弃它。甚至可以修改数据包 */

    // 在此示例中，我们简单地丢弃所有数据包
    return NF_DROP;           /* 丢弃所有数据包 */
}

/* 初始化函数 */
int init_module()
{   /* 填充我们的钩子结构 */
    nfho.hook = hook_func;         /* 处理器函数 */
    nfho.hooknum  = NF_INET_PRE_ROUTING; /* IPv4 的第一个钩子 */
    nfho.pf       = PF_INET;
    nfho.priority = NF_IP_PRI_FIRST;   /* 让我们的功能优先执行 */

    // nf_register_hook(&nfho);               // 对于 Ubuntu 16.04 VM
    nf_register_net_hook(&init_net, &nfho);   // 对于 Ubuntu 20.04 VM
    return 0;
}

/* 清理函数 */
void cleanup_module()
{
    // nf_unregister_hook(&nfho);             // 对于 Ubuntu 16.04 VM
    nf_unregister_net_hook(&init_net, &nfho); // 对于 Ubuntu 20.04 VM
}
\end{lstlisting}




% *******************************************
% SECTION
% ******************************************* 
\section{提交与演示}

\seedsubmission

\end{document}
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
```